package de.mmis.core.base.informationprovider;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.log4j.Logger;

import de.mmis.core.base.ClassFinder;
import de.mmis.core.base.gui.DeviceControlComponent;
import de.mmis.core.base.gui.DeviceRepresentationComponent;

/**
 * basic {@link DeviceInformationProvider} implementation implementing GUI
 * component search and propagating other requests to its parent provider if
 * present.
 * 
 * @author Christian Koch
 * 
 */
public class ComponentFinderDeviceInformationProvider extends
		PropagatingDeviceInformationProvider {

	private final static Logger LOGGER = Logger
			.getLogger(ComponentFinderDeviceInformationProvider.class);

	private ClassFinder finder;
	private Class<?>[] controlComponents;
	private Class<?>[] representationComponents;

	List<Class<?>> representationComponentList;
	List<Class<?>> controlComponentList;

	public enum SearchType {
		ResourceSearch, ClasspathSearch, None,
	}

	public ComponentFinderDeviceInformationProvider(SearchType type) {
		this(null, type);
	}

	public ComponentFinderDeviceInformationProvider(
			DeviceInformationProvider parent, SearchType type) {
		super(parent);

		switch (type) {
		case ResourceSearch:
			doResourceSearch();
			break;
		case ClasspathSearch:
			doClassPathSearch();
			break;
		case None:
			break;
		}
	}

	@SuppressWarnings("unchecked")
	private void processInput(InputStream is, URL url) {

		Properties p = new Properties();
		try {
			p.load(is);
		} catch (IOException e) {
			LOGGER.warn("WARNING: cannot open " + url + ", skipping", e);
			return;
		}

		Enumeration<String> keys = (Enumeration<String>) p.propertyNames();
		while (keys.hasMoreElements()) {
			String key = keys.nextElement();
			Class<?> c;
			try {
				c = Class.forName(key);
			} catch (ClassNotFoundException e) {
				LOGGER.warn("WARNING: cannot find class " + key
						+ " in classpath (declared in " + url + "), skipping",
						e);
				continue;
			}

			if (DeviceControlComponent.class.isAssignableFrom(c))
				this.controlComponentList.add(c);
			if (DeviceRepresentationComponent.class.isAssignableFrom(c))
				this.representationComponentList.add(c);
		}
	}

	/**
	 * do resource search to find components. Needs a resource directory
	 * "components" containing property files mapping with the component class
	 * names as its keys. Works with jars and local directories only.
	 */
	private void doResourceSearch() {
		LOGGER.debug("initializing resource search...");
		this.representationComponentList = new ArrayList<Class<?>>();
		this.controlComponentList = new ArrayList<Class<?>>();

		Enumeration<URL> urls;
		try {
			urls = getClass().getClassLoader().getResources("components");
		} catch (IOException e) {
			LOGGER.error("finding resource URLs failed:", e);
			return;
		}

		while (urls.hasMoreElements()) {
			URL url = urls.nextElement();

			LOGGER.debug("processing path " + url);

			File directory = new File(url.getFile());

			if (directory.exists()) {
				LOGGER.debug("path is existing local directory");
				for (File file : directory.listFiles()) {

					LOGGER.debug("processing file " + file);

					InputStream is;
					try {
						is = new FileInputStream(file);
					} catch (FileNotFoundException e) {
						LOGGER.warn("cannot open " + url + ", skipping", e);
						return;
					}
					processInput(is, url);
				}
			} else {
				try {
					LOGGER.debug("path is jar url");
					// It does not work with the filesystem: we must
					// be in the case of a package contained in a jar file.
					JarURLConnection conn = (JarURLConnection) url
							.openConnection();
					// String starts = conn.getEntryName();
					JarFile jarFile = conn.getJarFile();

					Enumeration<JarEntry> e = jarFile.entries();
					while (e.hasMoreElements()) {
						JarEntry entry = e.nextElement();
						String entryname = entry.getName();

						if (!entry.isDirectory()
								&& entryname.startsWith("components/")) {
							LOGGER.debug("found JarEntry " + entryname);
							InputStream is = jarFile.getInputStream(entry);
							processInput(is, url);
						}

					}
				} catch (IOException e) {
					LOGGER.error(
							"an IO exception occurred while processing jar "
									+ url, e);
				}
			}
		}

		LOGGER.debug("found " + this.controlComponentList.size()
				+ " control components and "
				+ this.representationComponentList.size()
				+ " representation components");

		this.controlComponents = this.controlComponentList
				.toArray(new Class<?>[this.controlComponentList.size()]);
		this.representationComponents = this.representationComponentList
				.toArray(new Class<?>[this.representationComponentList.size()]);
	}

	/**
	 * directly search class path for available components. not working when
	 * using web start ("java.class.path" not set properly)
	 */
	private void doClassPathSearch() {
		LOGGER.debug("initializing classpath search...");
		this.finder = new ClassFinder();

		LOGGER.debug("searching for control components... ");
		this.controlComponentList = this.finder
				.findSubclasses(DeviceControlComponent.class);

		LOGGER.debug(this.controlComponentList.size() + " found");
		if (this.controlComponentList.size() == 0)
			for (Throwable t : this.finder.getErrors())
				LOGGER.error("error while searching in classpath:", t);

		this.controlComponents = new Class<?>[this.controlComponentList.size()];
		for (int i = 0; i < this.controlComponents.length; i++)
			this.controlComponents[i] = this.controlComponentList.get(i);

		LOGGER.debug("searching for representation components... ");
		this.representationComponentList = this.finder
				.findSubclasses(DeviceRepresentationComponent.class);
		LOGGER.debug(this.representationComponentList.size() + " found");

		if (this.representationComponentList.size() == 0)
			for (Throwable t : this.finder.getErrors())
				LOGGER.error("error while searching in classpath:", t);

		this.representationComponents = new Class<?>[this.representationComponentList
				.size()];
		for (int i = 0; i < this.representationComponents.length; i++)
			this.representationComponents[i] = this.representationComponentList
					.get(i);
	}

	@Override
	public Class<?>[] getControlComponentClasses() {
		if (this.controlComponents != null)
			return this.controlComponents;

		return super.getControlComponentClasses();
	}

	@Override
	public Class<?>[] getRepresentationComponentClasses() {
		if (this.representationComponents != null)
			return this.representationComponents;

		return super.getRepresentationComponentClasses();
	}
}
